﻿#region References
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using JigLibX.Physics;
using JigLibX.Geometry;
using JigLibX.Collision;
using Xen;
using Xen.Camera;
using Xen.Graphics;
using Xen.Ex.Graphics;
using Xen.Ex.Graphics2D;
using Xen.Input.State;
using Xen.Input;
#endregion

namespace Ness.Engine.Cameras
{
    /// <summary>
    /// Simple 'First Person' style free roaming camera controlled by a <see cref="PlayerInput"/>
    /// </summary>
    sealed class FreeCamera : ControlledCamera3D
    {
        /// <summary>
        /// Construct the camera
        /// </summary>
        /// <param name="updateManager"></param>
        public FreeCamera(Engine game, UpdateManager updateManager)
            : this(game, updateManager, Vector3.Zero)
        {
        }
        /// <summary>
        /// Construct the camera
        /// </summary>
        /// <param name="updateManager"></param>
        /// <param name="startPosition"></param>
        public FreeCamera(Engine game, UpdateManager updateManager, Vector3 startPosition)
            : this(game, updateManager, startPosition, false)
        {
            this.zUp = false;
            CameraMatrix = Matrix.CreateLookAt(startPosition, startPosition + new Vector3(1, 0, 0), new Vector3(0, 0, 1));
            CameraMatrix += Matrix.Identity;
            this.Position = startPosition;
        }
        /// <summary>
        /// Construct the camera
        /// </summary>
        /// <param name="updateManager"></param>
        /// <param name="startPosition"></param>
        /// <param name="zUp">If true, the Z-Axis is treated as the up/down axis, otherwise Y-Axis is treated as up/down</param>
        public FreeCamera(Engine game, UpdateManager updateManager, Vector3 startPosition, bool zUp)
            : this(game, updateManager, startPosition, zUp, new Projection())
        {
            this.zUp = zUp;
            CameraMatrix = Matrix.CreateLookAt(startPosition, startPosition + new Vector3(1, 0, 0), new Vector3(0, 0, 1));
            CameraMatrix += Matrix.Identity;
            this.Position = startPosition;
        }
        /// <summary>
        /// Construct the camera
        /// </summary>
        /// <param name="updateManager"></param>
        /// <param name="startPosition"></param>
        /// <param name="zUp">If true, the Z-Axis is treated as the up/down axis, otherwise Y-Axis is treated as up/down</param>
        /// <param name="projection"></param>
        public FreeCamera(Engine game, UpdateManager updateManager, Vector3 startPosition, bool zUp, Projection projection)
            : base(game, updateManager, projection)
        {
            this.zUp = zUp;
            CameraMatrix = Matrix.CreateLookAt(startPosition, startPosition + new Vector3(1, 0, 0), new Vector3(0, 0, 1));
            CameraMatrix += Matrix.Identity;
            this.Position = startPosition;
        }

        private Vector2 rotAcceleration, move;

        private float v_cap = (float)Math.PI / 2.05f;
        private bool zUp;

        /// <summary>
        /// By default the Y-axis is up/down, set to true to make the Z-axis up/down.
        /// </summary>
        public bool ZAxisUp
        {
            get { if (IsDisposed) throw new ObjectDisposedException("this"); return zUp; }
            set { if (IsDisposed) throw new ObjectDisposedException("this"); zUp = value; }
        }

        /// <summary>
        /// Set the maximum angle the camera can look up/down
        /// </summary>
        public float VerticalAxisCap
        {
            get { if (IsDisposed) throw new ObjectDisposedException("this"); return v_cap; }
            set { if (IsDisposed) throw new ObjectDisposedException("this"); if (value > (float)Math.PI * 0.5f || value < 0) throw new ArgumentException(); v_cap = value; }
        }


        /// <summary></summary>
        /// <param name="state"></param>
        /// <param name="input"></param>
        protected override sealed void Update(UpdateState state, InputState input)
        {
            this.Projection.NearClip = 0.05f;
            this.Projection.FarClip = 1000f;

            Vector2 r = input.ThumbSticks.RightStick * (rotAcceleration * rotAcceleration * 0.05f + new Vector2(1, 1));

            viewRotation += r * RotationSensitivity * 0.04f;

            float cap = v_cap;

            if (viewRotation.Y > cap)
                viewRotation.Y = cap;
            if (viewRotation.Y < -cap)
                viewRotation.Y = -cap;

            rotAcceleration += input.ThumbSticks.RightStick * (new Vector2(0.5f, 0.25f) * (1.5f - move.Length() * 0.5f)) * 1.25f;
            rotAcceleration *= 0.9f;

#if !XBOX360
            if (state.PlayerInput[PlayerIndex].ControlInput == ControlInput.KeyboardMouse)
                rotAcceleration *= 0;//no rotation acceleration for mouse
#endif

            move *= 0.5f;
            move += input.ThumbSticks.LeftStick / 4;

            if (rotAcceleration.LengthSquared() > 0 && r.LengthSquared() > 0)
            {
                Vector2 v1 = rotAcceleration, v2 = r;
                v1.Normalize();
                v2.Normalize();
                float a = Vector2.Dot(v1, v2);
                if (a > 0)
                    rotAcceleration *= a;
                else
                    rotAcceleration *= 0.25f;
            }

            Matrix m1;

            if (zUp)
            {
                //z is up, so rotate left/right is around z.
                Matrix.CreateRotationZ(-viewRotation.X, out rotation);
                //x is always left to right, so rotate up/down is around x
                Matrix.CreateRotationX(viewRotation.Y + MathHelper.PiOver2, out m1);
            }
            else
            {
                //y is up, so rotate left/right is around y.
                Matrix.CreateRotationY(-viewRotation.X, out rotation);
                Matrix.CreateRotationX(viewRotation.Y, out m1);
            }

            Matrix.Multiply(ref m1, ref rotation, out rotation);

            Vector3 pos = Position;
           // rotation = rotation / 40;
            pos -= (rotation.Forward * move.Y * MovementSensitivity.Y * -8 + rotation.Left * move.X * MovementSensitivity.X * 8);// new Vector3(move.X * -8, 0, move.Y * 8);

            Matrix.CreateTranslation(ref pos, out m1);
            Matrix.Multiply(ref rotation, ref m1, out rotation);

            CameraMatrix = rotation;
        }

        /// <summary>
        /// Sets the <see cref="Camera3D.CameraMatrix"/> to a matrix that will make the camera look at a target
        /// </summary>
        /// <param name="cameraPosition"></param>
        /// <param name="lookAtTarget"></param>
        /// <param name="upVector"></param>
        /// <remarks>
        /// <para>Using <see cref="Matrix.CreateLookAt(Vector3,Vector3,Vector3)"/> is not recommended because it creats a View matrix, so it cannot be used for non-camera matrices. The <see cref="Camera3D.CameraMatrix"/> of a camera is the Inverse (<see cref="Matrix.Invert(Matrix)"/>) of the View Matrix (<see cref="ICamera.GetViewMatrix(out Matrix)"/>), so trying to set the camera matrix using Matrix.CreateLookAt will produce highly unexpected results.
        /// </para></remarks>
        public override void LookAt(ref Vector3 lookAtTarget, ref Vector3 cameraPosition, ref Vector3 upVector)
        {
            base.LookAt(ref lookAtTarget, ref cameraPosition, ref upVector);

            //update viewRotation
            Matrix matrix = this.CameraMatrix;
            Vector3 dir = new Vector3(matrix.M31, matrix.M32, matrix.M33);

            //this should be correct... :-)
            if (zUp)
            {
                viewRotation.X = (float)Math.Atan2(-dir.X, -dir.Y);
                viewRotation.Y = -(float)Math.Asin(dir.Z);
            }
            else
            {
                viewRotation.X = -(float)Math.Atan2(dir.X, dir.Z);
                viewRotation.Y = -(float)Math.Asin(dir.Y);
            }
        }
    }
}
